const Web3 = require('web3');
const erc20ABI = require('./erc20');
const pcsABI = require('./pancake');
// 一个通过私钥来对交易进行签名的包
const Tx = require('ethereumjs-tx');
const moment = require('moment');

// bsc测试网rpc
const bscRpc = 'https://data-seed-prebsc-1-s1.binance.org:8545/';
// 钱包地址
const walletAddress = '0x8DE444458335273343607cDcF5A7451C93903fc2';
// 接受方地址
const receiveAddress = '0x8DEF3674e9A146CF5037C22dcE3922D49c745789';
// 要发送的代币 DAI 合约地址
const DAIAddress = '0x8a9424745056Eb399FD19a0EC26A14316684e274';
// 私钥
const privateKey = '';
// pc router
const pancakeRouter = '0x9Ac64Cc6e4415144C455BD8E4837Fea55603e5c3';
// wbnb 地址
const wbnbadddress = '0xae13d989dac2f0debff460ac112a837c89baa7cd';
// ETH 合约地址
const ETHAddress = '0x8babbb98678facc7342735486c851abd7a0d17ca';
// USDT 合约地址
const USDTAddress = '0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684';
// BUSD 合约地址
const BUSDAddress = '0x78867bbeef44f2326bf8ddd1941a4439382ef2a7';

// 经过Buffer转化的私钥
const BprivateKey = Buffer.from(privateKey, 'hex');

// 连接到web3 rpc
const web3 = new Web3(new Web3.providers.HttpProvider(bscRpc));



// 一些工具函数

/**
 * @description 根据decimals获取以太币单位
 * @author dingzy
 * @param {number} [decimals=18]
 * @returns {string} 以太币单位名称 
 */
const getEtherUnit = (decimals = 18) => {
    let etherUnit = 'ether';
    switch (decimals) {
        case 3:
            etherUnit = "Kwei";
            break;
        case 6:
            etherUnit = 'mwei';
            break;
        case 9:
            etherUnit = 'gwei';
            break;
        case 12:
            etherUnit = 'microether';
            break;
        case 15:
            etherUnit = 'milliether';
            break;
        case 18:
            etherUnit = 'ether';
            break;
    }
    return etherUnit;
}

/**
 * @description 构建交易
 * @author dingzy
 * @param {*} txData
 * @returns {*} Tx实例对象
 */
const buildTransaction = async(txData) => {
    const { to, value, data } = txData;
    // 获取由指定地址发送的交易数量 return number
    let nonce = await web3.eth.getTransactionCount(walletAddress);
    // 设为15Gwei
    const gasPrice = web3.utils.toWei(30..toString(), 'Gwei');
    // 150000
    const gasLimit = 150000;
    return new Tx({
        from: walletAddress,
        to,
        data,
        nonce: web3.utils.toHex(nonce),
        gasPrice: web3.utils.toHex(gasPrice),
        gasLimit: web3.utils.toHex(gasLimit),
        value: web3.utils.toHex(value),
        chainId: 0x61
    });
}

// 分割线 ---------------------------------------------

/**
 * @description 获取BNB余额
 * @author dingzy
 * @returns {string} 
 */
const getBNBBalance = async() => {
    // 返回地址在指定区块的余额。
    const originBalance = await web3.eth.getBalance(walletAddress);
    // console.log(originBalance);
    // bnb的decimal为18位
    return web3.utils.fromWei(originBalance, getEtherUnit());
}


/**
 * @description 获取除BNB外其他代币余额
 * @author dingzy
 * @param {*} tokenaddress
 * @returns {string} 
 */
const getTokenBalance = async(tokenaddress) => {
    // 带有所有方法和事件的合约实例, contract对象可以调用erc20.js中所有方法
    const contract = new web3.eth.Contract(erc20ABI, tokenaddress);

    // 获取初始余额
    const originBalance = await contract.methods.balanceOf(walletAddress).call();
    // 获取代币对应精度
    const decimals = await contract.methods.decimals().call();
    return web3.utils.fromWei(originBalance, getEtherUnit(Number(decimals)));
}

const sendToken = async() => {
    // 1.构建交易 2.交易签名 3.发送交易 
    // erc20ABI中有transfer这个方法,入参dst,wad代表转发地址和转发数量，返回布尔值，调用该函数不需要支付bnb

    // 实例化Contract对象
    const tokenContract = new web3.eth.Contract(erc20ABI, DAIAddress);
    // 获取代币对应精度
    const decimals = await tokenContract.methods.decimals().call();
    // 代币数量
    const sendAmount = web3.utils.toWei(0.06.toString(), getEtherUnit(decimals));
    // 为指定的合约方法进行 ABI 编码，可用于发送交易、调用方法或向另一个合约方法传递参数。
    // 返回编码后的ABI字节码，可用于交易发送或方法调用。
    const data = tokenContract.methods.transfer(receiveAddress, sendAmount).encodeABI();
    const symbol = await tokenContract.methods.symbol().call();
    console.log(`要发送token名称为 ${symbol}`);
    const originBalance = await tokenContract.methods.balanceOf(walletAddress).call();
    console.log(`${symbol} 的余额为${originBalance}`);

    // 1.构建交易 转发除bnb以外的代币时value赋值0，另应将代币的合约地址赋给to，这样就可以调用103行合约的transfer方法
    const buildTx = await buildTransaction({
        to: DAIAddress,
        value: 0,
        data
    });
    // 2.交易签名
    buildTx.sign(BprivateKey);
    // 序列化交易
    const serializedTx = buildTx.serialize();
    // 3.发送交易
    const receipt = await web3.eth.sendSignedTransaction(`0x${serializedTx.toString('hex')}`);
    return receipt.status === true;
}

const sendBNB = async() => {
    // 1.构建交易 2.交易签名 3.发送交易 
    // 发送BNB时无需调用合约方法，因此构建交易时data字段为空
    // erc20ABI中有transfer这个方法,入参dst,wad代表转发地址和转发数量，返回布尔值，调用该函数不需要支付bnb

    // 代币数量
    const sendAmount = web3.utils.toWei(0.07.toString(), 'ether');

    // 1.构建交易 receiveAddress赋给to 
    const buildTx = await buildTransaction({
        to: receiveAddress,
        value: sendAmount,
        data: ''
    });
    // 2.交易签名
    buildTx.sign(BprivateKey);
    // 序列化交易
    const serializedTx = buildTx.serialize();
    // 3.发送交易
    const receipt = await web3.eth.sendSignedTransaction(`0x${serializedTx.toString('hex')}`);
    return receipt.status === true;
}

const swapBNBToToken = async() => {
    // 1.构建交易 2.交易签名 3.发送交易
    const tokenContract = new web3.eth.Contract(pcsABI, pancakeRouter);

    const sendContract = new web3.eth.Contract(erc20ABI, DAIAddress);
    // 滑点
    const los = 1;
    // 兑换比例
    const rate = 580;
    const bnbAmount = 0.05;
    // 将bnb数量进行转化
    const nbnb = web3.utils.toWei(String(bnbAmount), 'ether');
    const tokenAmount = bnbAmount * (100 - los) * 0.01 * rate;
    const decimals = await sendContract.methods.decimals().call();
    // swap相关参数
    const amountOutMin = web3.utils.toWei(String(tokenAmount), getEtherUnit(decimals));
    const routePath = [wbnbadddress, DAIAddress];
    const dealline = moment().format('X') + 60 * 20;

    const data = tokenContract.methods.swapExactETHForTokens(amountOutMin, routePath, walletAddress, dealline).encodeABI();

    // 1.构建交易 nbnb作为购买金额
    const buildTx = await buildTransaction({
        to: pancakeRouter,
        value: nbnb,
        data
    });
    // 2.交易签名
    buildTx.sign(BprivateKey);
    // 序列化交易
    const serializedTx = buildTx.serialize();
    // 3.发送交易
    const receipt = await web3.eth.sendSignedTransaction(`0x${serializedTx.toString('hex')}`);
    console.log(`Receipt info:  ${JSON.stringify(receipt, null, '\t')}`);
    return receipt.status === true;
}

// DAI <=> USDT
const swapTokenToToken = async() => {
    // 1.授权 2.构建交易 2.交易签名 3.发送交易
    const tokenContract = new web3.eth.Contract(pcsABI, pancakeRouter);

    const DAIContract = new web3.eth.Contract(erc20ABI, DAIAddress);
    const USDTContract = new web3.eth.Contract(erc20ABI, USDTAddress);
    // 滑点
    const los = 1;
    // 兑换比例
    const rate = 1;
    // 50个DAI
    const fromAmount = 1.2;
    const tokenAmount = fromAmount * (100 - los) * 0.01 * rate;

    const DAIdecimals = await DAIContract.methods.decimals().call();
    const USDTdecimals = await USDTContract.methods.decimals().call();
    // swap相关参数
    const amountIn = web3.utils.toWei(String(fromAmount), getEtherUnit(DAIdecimals));
    const amountOutMin = web3.utils.toWei(String(tokenAmount), getEtherUnit(USDTdecimals));
    const routePath = [USDTAddress, DAIAddress];
    const dealline = moment().format('X') + 60 * 20;

    const data = tokenContract.methods.swapExactTokensForTokens(amountIn, amountOutMin, routePath, walletAddress, dealline).encodeABI();
    // 1.判断psc地址是否被要交换的代币授权，若没有则对它进行授权
    const isApproved = await allowance(USDTContract, walletAddress, pancakeRouter);
    console.log(`当前token被授权余额为${web3.utils.fromWei(isApproved, 'ether')}`)
        // 2.如果未被授权或授权额过小
    if (+isApproved < amountIn) {
        const approveAmount = 2;
        const approveResult = await approveToken(USDTContract, USDTAddress, approveAmount);
        if (approveResult) {
            console.log('授权成功');
        } else {
            console.log('授权失败');
        }
    }

    // 3.构建交易 nbnb作为购买金额
    const buildTx = await buildTransaction({
        to: pancakeRouter,
        value: 0,
        data
    });
    // 4.交易签名
    buildTx.sign(BprivateKey);
    // 序列化交易
    const serializedTx = buildTx.serialize();
    // 5.发送交易
    const receipt = await web3.eth.sendSignedTransaction(`0x${serializedTx.toString('hex')}`);
    console.log(`Receipt info:  ${JSON.stringify(receipt, null, '\t')}`);
    return receipt.status === true;
}

/**
 * 
 * @param {*} tokenContract 
 * @param {*} owner 
 * @param {*} spender 
 * @returns string
 */
const allowance = async(tokenContract, owner, spender) => {
    return tokenContract.methods.allowance(owner, spender).call();
}

const approveToken = async(tokenContract, tokenAddress, approveAmount) => {
    approveAmount = web3.utils.toWei(String(approveAmount), getEtherUnit());
    const data = tokenContract.methods.approve(pancakeRouter, approveAmount).encodeABI();

    const buildTx = await buildTransaction({
        to: tokenAddress,
        value: 0,
        data
    });
    buildTx.sign(BprivateKey);
    const serializedTx = buildTx.serialize();
    const receipt = await web3.eth.sendSignedTransaction(`0x${serializedTx.toString('hex')}`);
    // console.log(`approve info:  ${JSON.stringify(receipt, null, '\t')}`);
    return receipt.status === true;
}

const main = async() => {
    // 1. 获取钱包余额bnb
    const BNBBalance = await getBNBBalance();
    console.log(`钱包账户余额为 ${BNBBalance} BNB`);

    // 2. 获取钱包其他token余额
    const tokenMap = new Map();
    tokenMap.set('USDT', '0x7ef95a0fee0dd31b22626fa2e10ee6a223f8a684');
    tokenMap.set('DAI', '0x8a9424745056eb399fd19a0ec26a14316684e274');
    tokenMap.set('ETH', '0x8babbb98678facc7342735486c851abd7a0d17ca');

    // 遍历获取每个token在钱包中的余额
    for (let [name, value] of tokenMap) {
        const tokenBalance = await getTokenBalance(value);
        console.log(`钱包地址 ${walletAddress} 中 ${name} 的余额为 ${tokenBalance}`);
    }

    // 3. 发送token到某个地址
    // const sendResult = await sendToken();
    // if (sendResult) {
    //     console.log('发送token成功')
    // } else {
    //     console.log('发送token失败')
    // }

    // 4.发送bnb
    // const sendBNBResult = await sendBNB();
    // if (sendBNBResult) {
    //     console.log('发送BNB成功');
    // } else {
    //     console.log('发送BNB失败');
    // }

    // 5.pancakeswap bnb swap
    // const swapResult = await swapBNBToToken();
    // if (swapResult) {
    //     console.log('兑换成功');
    // } else {
    //     console.log('兑换失败');
    // }

    // 6.pancakeswap token swap
    const tokenResult = await swapTokenToToken();
    if (tokenResult) {
        console.log('兑换成功')
    }
}

main();